/*
 * jdbc-trace-wrapper
 * Copyright (C) 2010. The jdbc-trace-wrapper Project
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package nz.jdbcwrapper;

import java.sql.SQLException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.Collection;
import java.util.Set;
import java.util.SortedSet;
import java.util.Arrays;
import java.util.Map.Entry;
import java.lang.reflect.Array;

class Util
{
  static final ThreadLocal localSdf = new ThreadLocal() {
    protected Object initialValue() {
      return new SimpleDateFormat("HH:mm:ss.SSS");
    }
  };

  static final ThreadLocal localDate = new ThreadLocal() {
    protected Object initialValue() {
      return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    }
  };

  static int timingsMaxSize = 1000;
  static final LinkedHashMap timings = new LinkedHashMap();
  static final List trace = new LinkedList();

  static final long DEFAULT_TRACE_TIMEOUT = 30; // seconds
  static long TRACE_TIMEOUT = DEFAULT_TRACE_TIMEOUT * 1000L; // milliseconds

  static final int DEFAULT_PRINT_LENGTH = 30;
  static int PRINT_LENGTH = DEFAULT_PRINT_LENGTH;

  static final int DEFAULT_MAX_ARRAY_ITEMS = 3;
  static int MAX_ARRAY_ITEMS = DEFAULT_MAX_ARRAY_ITEMS;

  private static long id = 0L;
  private static long line = 0L;
  private static int traceId = 0;
  private static Thread traceThread = null;
  private static final Statistic dummyStatistic = new Statistic();

  static synchronized long id()
  {
    return ++id;
  }

  static synchronized long line()
  {
    return ++line;
  }

  static Statistic register(String key)
  {
    if (timingsMaxSize <= 0)
      return dummyStatistic;

    key = key.intern();
    synchronized(timings)
    {
      Statistic stat = (Statistic)timings.get(key);
      if (stat == null)
      {
        int size = timings.size();
        int maxSize = timingsMaxSize > 0 ? timingsMaxSize : 0;
        if (size > maxSize) {
          Iterator iterator = timings.entrySet().iterator();
          do {
            iterator.next();
            iterator.remove();
          } while (--size > maxSize);
        }
        stat = new Statistic();
        timings.put(key, stat);
      }
      return stat;
    }
  }

  static void report(boolean incremental)
  {
    boolean any = false;

    synchronized(timings)
    {
      boolean allReported = true;

      for (Iterator it = timings.values().iterator(); it.hasNext(); )
      {
        Statistic s = (Statistic)it.next();
        if (!s.reported()) {
          s.markReported();
          allReported = false;
        }
      }
      if (allReported)
        return;

      for (Iterator it = timings.entrySet().iterator(); it.hasNext(); )
      {
        any = true;
        Entry e = (Entry)it.next();
        String sql = (String)e.getKey();
        Statistic s = (Statistic)e.getValue();

        WrapperDriver.out.println(line() + " " + sql + ": ");
        WrapperDriver.out.println(line() + " " + "  Executions: " + s.executions +
                           ", duration " + s.exDuration +
                           " ms;  Fetches: " + s.fetches +
                           ", duration " + s.fetchDuration +
                           " ms;  Columns: " + s.columns +
                           ", duration " + s.columnDuration);

        if (incremental)
          it.remove();
      }
    }
    if (any)
    {
      WrapperDriver.out.println(line() + " -------------------------------------------------------------------------------");
    }
  }

  /**
   * Register a begin-trace before a call.  The trace line will be
   * printed at the end-trace unless the end-trace takes too long, or
   * an intervening trace is requested.  In that case, an incomplete
   * stats is printed against the trace.
   *
   * @param message The message to be traced.
   * @return a handle to pass to the traceEnd call which will match up
   * with the traceBegin.
   */
  static long traceBegin(String message)
  {
    synchronized(trace)
    {
      long traceKey = traceId++;
      TraceItem item = new TraceItem(message,traceKey);
      trace.add(item);
      return traceKey;
    }

  }

  /**
   * Print the duration and return value of a traced call.  If the
   * call took too long or other calls have ended since this call
   * began, then its record will have been removed from the queue and
   * less information will be printed for it.
   *
   * @param key The handle returned by an earlier call to traceBegin()
   * @param result The call's return value to be logged against it.
   */

  static void traceEnd(long key, String result)
  {
    internalTraceEnd(key, result == null ? "" : " -> " + result);
  }

  /**
   * Print the duration and exception details of a traced call that
   * threw an exception.  If the call took too long or other calls
   * have ended since this call began, then its record will have been
   * removed from the queue and less information will be printed for
   * it.
   *
   * @param key The handle returned by an earlier call to traceBegin()
   * @param result The exception that the call threw to be recorded against it.
   */

  static void traceEnd(long key, Throwable result)
  {
    internalTraceEnd(key, reportException(result));
  }

  public static String toString(Object value)
  {
      return toString(value, PRINT_LENGTH, 2);
  }

  public static String toString(byte value)
  {
      return String.valueOf(value);
  }

  public static String toString(boolean value)
  {
      return String.valueOf(value);
  }

  public static String toString(char value)
  {
      return String.valueOf(value);
  }

  public static String toString(int value)
  {
      return String.valueOf(value);
  }

  public static String toString(long value)
  {
      return String.valueOf(value);
  }

  public static String toString(float value)
  {
      return String.valueOf(value);
  }

  public static String toString(double value)
  {
      return String.valueOf(value);
  }

  public static String toString(Object value, int printLength, int level)
  {
      if (value == null)
        return "<null>";

      if (value instanceof Number)
        return value.toString();

      if (value instanceof Character)
        return sanitize(((Character)value).charValue());

      if (value instanceof java.sql.Date)
        return value.toString();

      if (value instanceof java.sql.Time)
        return value.toString();

      if (value instanceof java.sql.Timestamp)
        return value.toString();

      if (value instanceof Date)
        return ((DateFormat)localDate.get()).format((Date)value);

      if (level > 0)
      {
          if (value instanceof Entry) {
              Entry e = (Entry) value;
              return toString(e.getKey(), printLength / 2, level - 1) + " -> " + toString(e.getValue(), printLength / 2, level - 1);
          }

          Iterator it = null;
          String type = null;
          int size = 0;

          if (value instanceof Map) {
              Map map = (Map) value;
              it = map.entrySet().iterator();
              size = map.size();
              type = map instanceof SortedMap ? "SortedMap[" : "Map[";
          }

          if (value instanceof Collection) {
              Collection collection = (Collection) value;
              it = collection.iterator();
              size = collection.size();
              if (collection instanceof Set) {
                  type = collection instanceof SortedSet ? "SortedSet" : "Set";
              }
              else {
                  type = collection instanceof List ? "List" : "Collection";
              }
          }

          if (value.getClass().isArray()) {
              int items = size > MAX_ARRAY_ITEMS ? MAX_ARRAY_ITEMS : size;
              Object[] array = new Object[items];
              for (int i = 0; i < items; ++i)
                array[i] = Array.get(value, i);
              it = Arrays.asList(array).iterator();
              size = Array.getLength(value);
              type = "Array";
          }

          if (it != null){
              StringBuffer sb = new StringBuffer(type);
              int count;
              for (count = 0; it.hasNext() && count < MAX_ARRAY_ITEMS; ++count) {
                  if (count > 0)
                    sb.append(", ");
                  sb.append(toString(it.next(), printLength / 2, level - 1));
              }

              if (count < size)
                sb.append(", ... (count=" + size + ")");
              sb.append(']');
              return sb.toString();
          }
      }
      CharSequence string;
      String suffix;
      StringBuffer result = new StringBuffer();

      if (value instanceof CharSequence) {
          string = (CharSequence) value;
          suffix = "\"";
          result.append('"');
      }
      else {
          string = value.toString();
          result.append(value.getClass().getName()).append('(');
          suffix = ")";
      }

      int length = string.length();
      char[] chars;
      if (length <= printLength) {
          chars = string.toString().toCharArray();
      }
      else {
          chars = string.subSequence(0, printLength).toString().toCharArray();
          suffix = "... (length=" + length + ")" + suffix;
      }

      for (int i = 0; i < chars.length; ++i)
        result.append(sanitize(chars[i]));
      result.append(suffix);
      return result.toString();
  }

    private static String sanitize(char ch) {
        switch (ch) {
            case '\n':
                return "\\n";
            case '\r':
                return "\\r";
            case '\t':
                return "\\t";
            case '\\':
                return "\\\\";
            case '"':
                return "\\\"";
            default:
                if (ch >= 0x20 && ch <= 0x7E) {
                    return String.valueOf(ch);
                }
                String hex = Integer.toHexString(ch & 0xFFFF).toUpperCase();
                if (hex.length() < 4)
                    hex = "0000".substring(hex.length(), 4) + hex;
                return "\\u" + hex;
        }
    }


    private static void internalTraceEnd(long key, String result)
    {
      synchronized (trace)
      {
        boolean found = false;

        /* Clear the call list of all methods that began before this one
        * and which are yet to finish.
        */

        for (Iterator it = trace.iterator(); it.hasNext(); )
        {
          TraceItem item = (TraceItem)it.next();

          if (item.key < key)
            item.unfinished();
          else
          {
            it.remove();
            if (item.key == key)
            {
              found = true;
              item.print(result);
            }

            break;
          }
        }

        if (!found)
          TraceItem.print(null, 0, "<... finished(" + key + ") >" + result);

        // TraceItem.stackTrace();
      }
    }

  static void trace(Throwable exception)
  {
    trace(reportException(exception));
  }

  static void trace(String message)
  {
    synchronized (trace)
    {
      for (Iterator it = trace.iterator(); it.hasNext(); )
      {
        TraceItem item = (TraceItem)it.next();
        item.unfinished();
      }
      TraceItem.print(null, 0, message);
    }
  }

  static void startTraceThread(String threadname, Data data)
  {
    synchronized(trace)
    {
      String timeout = data.options.getProperty(WrapperDriver.TIMEOUT_KEY);
      if (timeout != null)
      {
        try
        {
          long value = Long.parseLong(timeout.trim());
          if (value == 0)
            value = DEFAULT_TRACE_TIMEOUT;

          if (value > 0)
            TRACE_TIMEOUT = value * 1000L;
        }
        catch (NumberFormatException e)
        {
          // ignore
        }
      }

      String printLength = data.options.getProperty(WrapperDriver.PRINT_LENGTH_KEY);
      if (printLength != null)
      {
        try
        {
          int value = Integer.parseInt(printLength.trim());
          if (value == 0)
            value = DEFAULT_PRINT_LENGTH;

          if (value > 0)
            PRINT_LENGTH = value;
        }
        catch (NumberFormatException e)
        {
          // ignore
        }
      }

      String maxItems = data.options.getProperty(WrapperDriver.MAX_ARRAY_KEY);
      if (maxItems != null)
      {
        try
        {
          int value = Integer.parseInt(maxItems.trim());
          if (value == 0)
            value = DEFAULT_MAX_ARRAY_ITEMS;

          if (value > 0)
            MAX_ARRAY_ITEMS = value;
        }
        catch (NumberFormatException e)
        {
          // ignore
        }
      }

      if (traceThread == null)
      {
        traceThread = new Trace();
        traceThread.setName(threadname);
        traceThread.setDaemon(true);
        traceThread.start();
      }
    }
  }

  static void reportSQLException(StringBuffer sb, SQLException exception)
  {
    boolean multiple = exception.getNextException() != null;
    if (multiple)
      sb.append('[');
    boolean first = true;
    do
    {
      if (first)
      {
        first = false;
      }
      else
      {
        sb.append("; ");
      }
      sb.append(exception);

      exception = exception.getNextException();
    } while (exception != null);
    if (multiple)
    	sb.append(']');
  }

  static String reportException(Throwable exception)
  {
    StringBuffer sb = new StringBuffer(500);
    sb.append(" ****** ");
    boolean first = true;

    do
    {
      if (first)
      {
        first = false;
      }
      else
      {
        sb.append("; ");
      }

      if (exception instanceof SQLException)
        reportSQLException(sb, (SQLException)exception);
      else
        sb.append(exception);

      exception = exception.getCause();
    } while (exception != null);

    return sb.toString();
  }

  private static class TraceItem
  {
    final String message;
    final long timeOfOperation = System.currentTimeMillis();
    final long start = System.nanoTime();
    final long key;
    final String thread = Thread.currentThread().getName();
    boolean split = false;

    TraceItem(String message, long key)
    {
      this.message = message;
      this.key = key;
    }

    void unfinished()
    {
      if (!split)
      {
        print(thread, timeOfOperation, message + "  <unfinished(" + key + ") ...>");
        split = true;
      }
    }

    void print(String result)
    {
      long duration = (System.nanoTime() - start) / 1000L;

      if (split)
      {
        print(thread, timeOfOperation, "<... finished(" + key + ") >" + message + result + " (" + duration + "us)");
      }
      else
      {
        print(thread, timeOfOperation, message + result + " (" + duration + "us)");
      }
    }

    static void print(String threadName, long timeOfOperation, String message)
    {
      if (threadName == null)
      {
        threadName = Thread.currentThread().getName();
        timeOfOperation = System.currentTimeMillis();
      }

      String time = ((DateFormat)localSdf.get()).format(new Date(timeOfOperation));
      WrapperDriver.out.println(line() + " " + time + " <" + threadName + ">: " + message);
    }

    static void stackTrace()
    {
      new Throwable("Stack trace").printStackTrace(WrapperDriver.out);
    }
  }

  private static class Trace extends Thread
  {
    public void run()
    {
      try
      {
        sleep(500); // Slow start to avoid busy loop if thread always dies!

        for (;;)
        {
          long sleeptime = TRACE_TIMEOUT;

          synchronized(trace)
          {
            long now = System.currentTimeMillis();

            for (Iterator it = trace.iterator(); it.hasNext(); )
            {
              TraceItem item = (TraceItem)it.next();
              if (item.timeOfOperation + TRACE_TIMEOUT > now)
              {
                sleeptime = item.timeOfOperation + TRACE_TIMEOUT - now;
                break;
              }

              item.unfinished();
              it.remove();
            }
          }
          sleep(sleeptime);
        }
      }
      catch (Throwable t)
      {
        WrapperDriver.out.println(line() + " Thread " + getName() + " ended: " + t);

        synchronized(trace)
        {
          traceThread = new Trace();
          traceThread.setName(getName());
          traceThread.setDaemon(true);
          traceThread.start();
        }
      }
    }
  }
}
